一直以來專注在前端領域的我,有一天突然聽到身邊的朋友跟我說:「你是寫全端的喔?」我黑人問號了一下,回他:「不是,我是寫前端的,我是前端工程師。」
一來一往的對話後才發現,原來是因為我現在主要是寫 Next.js 框架,所以他就覺得用了全端框架我是寫全端的人。這時候我也才意識到「對耶!Next.js 其實是全端框架」。
今天就從為什麼 Next.js 是一個全端框架開始開啟今天的主題!
在多數人印象裡,Next.js 是一個能幫助 React 實現 SSR 的前端框架,但如果仔細去了解 Next.js 提供的功能後,就可以發現到 Next.js 已經不只是一個單純幫助前端以 SSR 開發的框架,因為它還涵蓋著實作 API 的功能,也就是 「Page Router 的 API Route」及「App Router 的 Route Handlers」,這也使得 Next.js 這個框架成為了一個「全端框架」。
如果使用 Next.js 進行全端專案的開發,也就不用另外建立一個後端專案來建立 API,只需要透過 Next.js 提供的 API Route 就可以建立 API。
當我們在寫 React 專案時,沒有辦法在專案內寫 API 的部分,如果想要一條龍自己完成,就只能額外建立一個後端的專案處理 API 的部分。但是用 Next.js 來寫專案的話,只需要透過 API Routes 或 Route Handlers,就可以在專案內建立 API,讓前端專案瞬間變成全端專案。
∙ Page Router:使用 API Routes
在 Page Router 中是使用 API Routes 的寫法來建立 API。這個寫法是在 pages 資料夾底下建立 api 資料夾,再以特定 endpoint 建立檔案。
這裡我們在 pages/api 資料夾底下建立一個 time 檔案,就可以把 endpoint 為 api/time
的 API 建立出來。
// src/pages/api/time.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const url = new URL(req.url!, `http://${req.headers.host}`);
// 模擬慢 1.5 秒
await new Promise((r) => setTimeout(r, 1500));
return res.status(200).json({ ts: new Date().toISOString() });
}
// 其他 HTTP 方法
res.setHeader("Allow", ["GET"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
這麼做之後就可以在我要的頁面中使用這個 API。
// pages/index.tsx
import { GetServerSideProps } from "next";
type Props = {
ts: string;
};
const Page = ({ ts }: Props) => {
return (
<div className="w-full h-[300px] flex justify-center items-center">
<p className="text-2xl">現在時間:{ts}</p>
</div>
);
};
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch("http://localhost:3001/api/time");
const data = await res.json();
return {
props: {
ts: data.ts,
},
};
};
export default Page;
∙ App Router:使用 Route Handlers
在 App Router 中是使用 Route Handlers 的寫法來建立 API。在 app 資料夾底下建立名為 api 的資料夾,再建立特定 endpoint 的資料夾,並於資料夾底下建立名為 route 的檔案,就可以將 API 建立出來。
當我們這樣設定檔案後,我們就可以建立出 endpoint 為 /api/time
的 API。
// src/app/api/time/route.ts
import { NextResponse } from "next/server";
export async function GET(req: Request) {
const url = new URL(req.url);
// 模擬慢 1.5 秒
await new Promise((r) => setTimeout(r, 3000));
return NextResponse.json({ key, ts: new Date().toISOString() });
}
這樣建立好一個 API 後,就可以在前端部分的程式碼中,這樣就使用 API。
const Page = () => {
const res = await fetch("http://localhost:3000/api/time", {
cache: "force-cache",
});
const data = await res.json();
return (
<div className="w-full h-[300px] flex justify-center items-center">
<p className="text-2xl">現在時間:{data.ts}</p>
</div>
);
不論是 API Routes 還是 Route Handlers 都是在 Next.js 中用來建立 API 的方式,差異只是在寫法上有些許的不同。另外,除了設定 GET API,當然也還可以設定 POST、PUT、DELETE 這幾個方法的 API。
但是必須要記得 Next.js 只是提供了 HTTP 的接口,讓瀏覽器可以透過 Next.js 提供的 API 發送 GET、POST 等的 request。也就是說 Next.js 只會幫我們處理 request,並幫忙回傳 response,並不會幫我們處理資料的部分,我們還是需要使用一些關於資料庫的外部套件,來取得資料返回給瀏覽器。除了作為 API 的用途外,也可以作為代理伺服器來呼叫第三方 API。
在 Next.js 中,除了可以建立接收 request 和發送 response 的 API 外,在使用 App Router 時,還提供了可以直接接觸到後端 DB 的方式 - Server Actions。
Server Actions 嚴格來說並不是 API,只是另一種可以接觸到後端的方式。
這裡也直接透過實際的例子來了解這邊的直接接觸 DB 是什麼樣的情況。
先建立一個 action 檔案,在裡面準備了 addMessage 這個直接接觸 Firebase 的 action。透過這個寫法,我們不需要再透過一層 API 去接受 request,才碰到 DB,而是直接接觸到 DB。
"use server";
import { db } from "@/lib/firebase-admin";
export async function addMessage(formData: FormData) {
const text = String(formData.get("text") ?? "").trim();
if (!text) throw new Error("text is required");
await db.ref("messages").push({ text, ts: Date.now() });
}
Server Actions 通常會和 form 的 action 一起使用(但是不限於使用在 form action 上),所以在建立完 addMessage 後,在元件中就可以透過 form action 去觸發 addMessage 並且直接新增值到後端 DB 上。
"use client";
import { use, useOptimistic, useRef, useTransition } from "react";
import { addMessage } from "../actions";
import { useRouter } from "next/navigation";
const FormAndList = ({ messages }: { messages: any[] }) => {
const router = useRouter();
const action = async (formData: FormData) => {
const text = String(formData.get("text") ?? "").trim();
if (!text) return;
await addMessage(formData);
router.refresh();
};
return (
<div className="flex flex-col gap-2 w-full">
<div className="w-full">
<form className="" action={action}>
<input
className="rounded-[4px] border bg-gray-50 h-8"
name="text"
/>
<button
className="rounded-[4px] w-16 text-white cursor-pointer h-8 bg-blue-400 px-2 py-auto ml-2"
type="submit"
>
Send
</button>
</form>
</div>
<div className="w-full">
<ul className="border rounded-[4px] p-2">
{messages.length > 0 &&
messages?.map((m) => <li key={m.id}>{m.text}</li>)}
</ul>
</div>
</div>
);
};
export default FormAndList;
最後就可以達成這樣的效果。
雖然實際上卻沒有發送任何 request,但還是有更新到 firebase 的資料。
這裡也分為三個面向比較兩者差異。
∙ 更新獲取後端資料的流程:API Routes/Route Handlers
:Client 端發送 HTTP request → API route → DB → 回傳 ResponseServer Actions
:Client 呼叫 Action → 直接接到 Server 端 function → DB → 回傳結果
∙ 開發相關:API Routes/Route Handlers
:是傳統的 API 開發方式,所以需要處理 request parsing、CORS 等。Server Actions
:可以像使佣一般的函式直接呼叫使用,不需要額外處理 API 的 request 和 CORS 等部分。
∙ 使用範圍:API Routes/Route Handlers
:可提供外部使用(Web、App、第三方服務),也能串接外部 API。Server Actions
:只能使用於內部,且只能在 Next.js 的 App Router 中使用。
簡單來說,如果要求泛用性,會建議使用透過 API Routes 或 Route Handlers 建立 API,如果單純只是內部專案的資料存取,則可以考慮使用 Server Actions。
Next.js 雖然是以 React 為基底的框架,但是他提供的功能已經不只限於前端,還有能讓 Server 能被活用多一點的後端功能,這些功能也就是 Page Router 的 API Routes,以及 App Router 的 Route Handlers 和 Server Actions。
在 Next.js 中透過 API Routes 或 Route Handlers 建立 API 和使用 Server Actions 的差異在於當我們使用 Server Actions 時,不需要發送 request,就可以接觸到 DB,所以如果今天的使用情境是不想要讓他人知道 API endpoint 的話,就可以使用 Server Actions。但也因為 Server Actions 有這個不用發送 request 就可以碰觸到 DB 的特性,所以只適用於自己的內部專案,無法提供給外部專案使用。相反的,當我們使用 API Routes 或 Route Handler 時,因為是傳統 API 的形式,所以可以被使用於專案外部,也可以串接外部的 API。
官方文件 - API Routes
官方文件 - Route Handlers
官方文件 - Server Actions